﻿using System;
using System.Net;
using System.Net.Sockets;
using System.Threading;
using System.Threading.Tasks;

namespace EasySocket
{
    public class EasySocketTcpClient : IEasySocketTcpClient
    {
        private EasySocketClientOption _option;
        private IEasySocketClientHandler _handler;
        private Socket _socket;
        private SocketAsyncEventArgs _connectArgs;
        private SocketAsyncEventArgs _readArgs;
        private SocketAsyncEventArgs _writeArgs;
        private bool _isDisposed;
        private SemaphoreSlim _lock;

        public async Task StartAsync(EasySocketClientOption option)
        {
            Dispose();
            _option = option ?? throw new ArgumentNullException(nameof(option));
            _handler = _option.Handler;
            IPEndPoint localEndPoint;
            _isDisposed = false;
            if (_option.UseIPV6)
            {
                _socket = new Socket(AddressFamily.InterNetworkV6, SocketType.Stream, ProtocolType.Tcp);
                if (string.IsNullOrEmpty(_option.LocalIP))
                    localEndPoint = (new IPEndPoint(IPAddress.IPv6Any, _option.LocalPort));
                else
                    localEndPoint = (new IPEndPoint(IPAddress.Parse(_option.LocalIP), _option.LocalPort));
            }
            else
            {
                _socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
                if (string.IsNullOrEmpty(_option.LocalIP))
                    localEndPoint = (new IPEndPoint(IPAddress.Any, _option.LocalPort));
                else
                    localEndPoint = (new IPEndPoint(IPAddress.Parse(_option.LocalIP), _option.LocalPort));
            }

            if (_option.ReusePort)
                _socket.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, _option.ReusePort);

            _socket.NoDelay = _option.NoDelay;
            _socket.SendBufferSize = _option.WriteBufferSize;
            _socket.ReceiveBufferSize = _option.ReadBufferSize;
            _socket.KeepAlive(_option.KeepAlive, _option.KeepInterval);
            _connectArgs = new SocketAsyncEventArgs
            {
                RemoteEndPoint = new IPEndPoint(IPAddress.Parse(_option.RemoteIP), _option.RemotePort)
            };
            _connectArgs.Completed += ConnectArgs_Completed;
            _socket.Bind(localEndPoint);
            //connect
            _lock = new SemaphoreSlim(1, 1);
            await _lock.WaitAsync();
            try
            {
                if (!_socket.ConnectAsync(_connectArgs))
                    await ProcessConnected(_connectArgs);
            }
            catch
            {
                EasySocketTcpServer.ReleseLock(_lock);
            }
        }

        public async Task SendAsync(byte[] data)
        {
            try
            {
                if (data == null || data.Length == 0)
                    throw new ArgumentNullException(nameof(data));

                if (_socket == null || !_socket.Connected || _isDisposed)
                {
                    await EasySocketTcpServer.IgnoreErrorHandler(() => _handler?.StateChangeAsync(new EasySocketClientState(ConnectionStateEnum.Disconnected)));
                    throw new Exception("socket connection is disconnected");
                }

                await _lock?.WaitAsync();
                if (_writeArgs == null)
                    _writeArgs = CreateArgs(SocketEventTypeEnum.Write);
                _writeArgs.SetBuffer(data, 0, data.Length);
                if (!_socket.SendAsync(_writeArgs))
                    ProcessSended(_writeArgs);
            }
            catch (Exception ex)
            {
                EasySocketTcpServer.ReleseLock(_lock);
                await DisconnectAsync();
                throw ex;
            }
        }


        public void Dispose()
        {
            DisconnectAsync().ConfigureAwait(false).GetAwaiter().GetResult();
            _isDisposed = true;
            _option = null;
            _socket = null;
            _handler?.Dispose();
            DisposeArgs(ref _connectArgs, ConnectArgs_Completed);
            DisposeArgs(ref _writeArgs, IO_Completed);
            DisposeArgs(ref _readArgs, IO_Completed);
            _lock?.Dispose();
        }

        #region private methods
        async void ConnectArgs_Completed(object sender, SocketAsyncEventArgs e)
        {
            if (e.LastOperation == SocketAsyncOperation.Connect)
               await  ProcessConnected(e);
            else
            {
                await EasySocketTcpServer.IgnoreErrorHandler(() => _handler?.HandleErrorAsync(new Exception($"connect fail: {e.SocketError}")));
                EasySocketTcpServer.ReleseLock(_lock);
            }
        }

        async Task ProcessConnected(SocketAsyncEventArgs e)
        {
            var connected = e.SocketError == SocketError.Success;
            if (connected)
            {
                _readArgs = CreateArgs(SocketEventTypeEnum.Read, _option.ReadBufferSize);
                await ProcessReceive(_readArgs);
            }

            await EasySocketTcpServer.IgnoreErrorHandler(() => _handler?.HandleErrorAsync(new Exception($"connect fail: {e.SocketError}")));
            EasySocketTcpServer.ReleseLock(_lock);
        }

        async Task ProcessReceive(SocketAsyncEventArgs readArgs)
        {
            if (_socket != null)
            {
                if (!_socket.ReceiveAsync(readArgs))
                    await ProcessReceived(readArgs);
            }

        }

        async Task ProcessReceived(SocketAsyncEventArgs readArgs)
        {
            try
            {
                if (readArgs.BytesTransferred > 0 && readArgs.SocketError == SocketError.Success)
                {
                    var buffer = readArgs.Buffer.AsSpan().Slice(readArgs.Offset, readArgs.BytesTransferred).ToArray();
                    try
                    {
                        await _handler?.ReceiveDataAsync(buffer);
                    }
                    catch (Exception ex)
                    {
                        await EasySocketTcpServer.IgnoreErrorHandler(() => _handler?.HandleErrorAsync(ex));
                    }

                    await ProcessReceive(readArgs);
                }
                else
                {
                   await DisconnectAsync();
                }
            }
            catch (Exception ex)
            {
                await EasySocketTcpServer.IgnoreErrorHandler(() => _handler?.HandleErrorAsync(ex));
                await DisconnectAsync();
            }
        }

        async void IO_Completed(object sender, SocketAsyncEventArgs e)
        {
            switch (e.LastOperation)
            {
                case SocketAsyncOperation.Receive:
                    await ProcessReceived(e);
                    break;
                case SocketAsyncOperation.Send:
                    ProcessSended(e);
                    break;
                default:
                    await DisconnectAsync();
                    break;
            }
        }

        private async Task DisconnectAsync()
        {
            try
            {
                _socket?.Shutdown(SocketShutdown.Both);
                _socket?.Close();
                _socket?.Dispose();
            }
            catch { }
            finally
            {
                await EasySocketTcpServer.IgnoreErrorHandler(() => _handler?.StateChangeAsync(new EasySocketClientState(ConnectionStateEnum.Disconnected)));
            }
        }

        private void DisposeArgs(ref SocketAsyncEventArgs args, EventHandler<SocketAsyncEventArgs> eventHandler)
        {
            if (args != null)
            {
                args.Completed -= eventHandler;
                args.Dispose();
                args = null;
            }
        }

        void ProcessSended(SocketAsyncEventArgs e)
        {
            EasySocketTcpServer.ReleseLock(_lock);
        }

        SocketAsyncEventArgs CreateArgs(SocketEventTypeEnum argsType, int? buffer = 0)
        {
            var args = new SocketAsyncEventArgs();
            args.Completed += IO_Completed;
            switch (argsType)
            {
                case SocketEventTypeEnum.Read:
                    args.SetBuffer(new byte[buffer.Value], 0, buffer.Value);
                    break;
            }

            return args;
        }
        #endregion
    }
}
